Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Node / ReticulumProjects / MeshChatX.git / files / meshchatx / src / backend / telephone_manager.py

Displaying Raw • Download


meshchatx/src/backend/telephone_manager.py 026b8c5bc7855095c2eb4e1cc43329493f8bad57 (026b8c5b) Text, 31.23 KB

T8b949e# SPDX-License-Identifier: 0BSD

Tff7b72import T7ee787asyncio
Tff7b72import T7ee787base64
Tff7b72import T7ee787contextlib
Tff7b72import T7ee787os
Tff7b72import T7ee787threading
Tff7b72import T7ee787time

Tff7b72import T7ee787RNS
Tff7b72from T7ee787LXST Tff7b72import Te6edf3Telephone

Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backend Tff7b72import Te6edf3reticulum_pathfinding
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787meshchat_utils Tff7b72import Tb4b4b4(
Te6edf3hex_identifier_to_bytesTb4b4b4,
Te6edf3normalize_hex_identifierTb4b4b4,
Tb4b4b4)


Tff7b72class T56d364TeeTb4b4b4:
Tff7b72def Tff7b72__init__Tb4b4b4(Tff7b72selfTb4b4b4, Te6edf3sinkTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffsinks Tff7b72= Tb4b4b4[Te6edf3sinkTb4b4b4]

Tff7b72def Td2a8ffadd_sinkTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3sinkTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3sink Tff7b72not Tff7b72in Tff7b72selfTff7b72.Td2a8ffsinksTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffsinksTff7b72.Td2a8ffappendTb4b4b4(Te6edf3sinkTb4b4b4)

Tff7b72def Td2a8ffremove_sinkTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3sinkTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3sink Tff7b72in Tff7b72selfTff7b72.Td2a8ffsinksTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffsinksTff7b72.Td2a8ffremoveTb4b4b4(Te6edf3sinkTb4b4b4)

Tff7b72def Td2a8ffhandle_frameTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3frameTb4b4b4, Te6edf3sourceTb4b4b4)Tb4b4b4:
Tff7b72for Te6edf3sink Tff7b72in Tff7b72selfTff7b72.Td2a8ffsinksTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3sinkTff7b72.Td2a8ffhandle_frameTb4b4b4(Te6edf3frameTb4b4b4, Te6edf3sourceTb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Te6edf3RNSTff7b72.Td2a8fflogTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffTee: Error in sink handle_frame: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4, Te6edf3RNSTff7b72.Td2a8ffLOG_ERRORTb4b4b4)

Tff7b72def Td2a8ffcan_receiveTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3from_sourceTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
Tff7b72return Tffa657anyTb4b4b4(Te6edf3sinkTff7b72.Td2a8ffcan_receiveTb4b4b4(Te6edf3from_sourceTb4b4b4) Tff7b72for Te6edf3sink Tff7b72in Tff7b72selfTff7b72.Td2a8ffsinksTb4b4b4)


Tff7b72class T56d364TelephoneManagerTb4b4b4:
T8b949e# LXST Status Constants for reference:
T8b949e# 0: STATUS_BUSY
T8b949e# 1: STATUS_REJECTED
T8b949e# 2: STATUS_CALLING
T8b949e# 3: STATUS_AVAILABLE
T8b949e# 4: STATUS_RINGING
T8b949e# 5: STATUS_CONNECTING
T8b949e# 6: STATUS_ESTABLISHED

Tff7b72def Tff7b72__init__Tb4b4b4(
Tff7b72selfTb4b4b4,
Te6edf3identityTb4b4b4: Te6edf3RNSTff7b72.Td2a8ffIdentityTb4b4b4,
Te6edf3config_managerTff7b72=Tff7b72NoneTb4b4b4,
Te6edf3storage_dirTff7b72=Tff7b72NoneTb4b4b4,
Te6edf3dbTff7b72=Tff7b72NoneTb4b4b4,
Tb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffidentity Tff7b72= Te6edf3identity
Tff7b72selfTff7b72.Td2a8ffconfig_manager Tff7b72= Te6edf3config_manager
Tff7b72selfTff7b72.Td2a8ffstorage_dir Tff7b72= Te6edf3storage_dir
Tff7b72selfTff7b72.Td2a8ffdb Tff7b72= Te6edf3db
Tff7b72selfTff7b72.Td2a8ffget_name_for_identity_hash Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffrecordings_dir Tff7b72= Tb4b4b4(
Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3storage_dirTb4b4b4, Ta5d6ff"Ta5d6ffrecordingsTa5d6ff"Tb4b4b4) Tff7b72if Te6edf3storage_dir Tff7b72else Tff7b72None
Tb4b4b4)
Tff7b72if Tff7b72selfTff7b72.Td2a8ffrecordings_dirTb4b4b4:
Te6edf3osTff7b72.Td2a8ffmakedirsTb4b4b4(Tff7b72selfTff7b72.Td2a8ffrecordings_dirTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)

Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffon_ringing_callback Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffon_established_callback Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffon_ended_callback Tff7b72= Tff7b72None

Tff7b72selfTff7b72.Td2a8ffcall_start_time Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffcall_status_at_end Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffcall_is_incoming Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffcall_was_established Tff7b72= Tff7b72False

T8b949e# Manual mute overrides in case LXST internal muting is buggy
Tff7b72selfTff7b72.Td2a8fftransmit_muted Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffreceive_muted Tff7b72= Tff7b72False
T8b949e# Half-duplex PTT state. LXST squelches the packetizer when TX is idle.
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffcall_stats Tff7b72= Tb4b4b4{Tb4b4b4}

Tff7b72selfTff7b72.Td2a8ffinitiation_status Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffinitiation_target_hash Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffon_initiation_status_callback Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ff_path_poll_interval_s Tff7b72= T79c0ff0.05
Tff7b72selfTff7b72.Td2a8ff_path_retry_interval_s Tff7b72= T79c0ff1.5
Tff7b72selfTff7b72.Td2a8ff_status_poll_interval_s Tff7b72= T79c0ff0.1
Tff7b72selfTff7b72.Td2a8ffis_voicemail_session_active Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffpreferred_profile_id Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ff_caller_allowed Tff7b72= Tff7b72None
Tff7b72selfTff7b72.Td2a8ff_blocked_identity_hashes Tff7b72= Tff7b72None
T8b949e# When True, LXST must not open PulseAudio LineSource/LineSink (Docker /
T8b949e# headless / Android web bridge). Set by ReticulumMeshChat.
Tff7b72selfTff7b72.Td2a8ffweb_audio_required Tff7b72= Tff7b72False

Tf0883e@property
Tff7b72def Td2a8ffis_recordingTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72False

Tf0883e@staticmethod
Tff7b72def Td2a8ffcodec2_availableTb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657boolTb4b4b4:
T8b949e"""Return whether LXST can construct Codec2 codecs (pycodec2 + libcodec2)."""
Tff7b72tryTb4b4b4:
Tff7b72from T7ee787meshchatx Tff7b72import Te6edf3android_codec2

Tff7b72if Te6edf3android_codec2Tff7b72.Td2a8ff_is_chaquopy_androidTb4b4b4(Tb4b4b4)Tb4b4b4:
Te6edf3okTb4b4b4, Te6edf3_err Tff7b72= Te6edf3android_codec2Tff7b72.Td2a8ffprobe_pycodec2Tb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3okTb4b4b4:
Tff7b72return Tff7b72False
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72tryTb4b4b4:
Tff7b72from T7ee787LXSTT7ee787.T7ee787Codecs Tff7b72import Te6edf3Codec2

Tff7b72if Te6edf3Codec2 Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return Tff7b72False
T8b949e# Touch a mode constant and construct once to catch dlopen failures early.
Te6edf3_ Tff7b72= Te6edf3Codec2Tff7b72.Td2a8ffCODEC2_1600
Te6edf3Codec2Tb4b4b4(Te6edf3modeTff7b72=Te6edf3Codec2Tff7b72.Td2a8ffCODEC2_1600Tb4b4b4)
Tff7b72return Tff7b72True
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72return Tff7b72False

Tff7b72def Td2a8ffresolve_audio_profile_idTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3profile_idTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Return a valid LXST profile id, falling back when Codec2 is unavailable."""
Tff7b72from T7ee787LXSTT7ee787.T7ee787PrimitivesT7ee787.T7ee787Telephony Tff7b72import Te6edf3Profiles

Te6edf3available Tff7b72= Tffa657setTb4b4b4(Te6edf3ProfilesTff7b72.Td2a8ffavailable_profilesTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3pid Tff7b72= Te6edf3profile_id
Tff7b72if Te6edf3pid Tff7b72is Tff7b72None Tff7b72and Tff7b72selfTff7b72.Td2a8ffconfig_managerTb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3pid Tff7b72= Tff7b72selfTff7b72.Td2a8ffconfig_managerTff7b72.Td2a8fftelephone_audio_profile_idTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3pid Tff7b72= Tffa657intTb4b4b4(Te6edf3pidTb4b4b4) Tff7b72if Te6edf3pid Tff7b72is Tff7b72not Tff7b72None Tff7b72else Tff7b72None
Tff7b72except Tb4b4b4(Tf85149TypeErrorTb4b4b4, Tf85149ValueErrorTb4b4b4)Tb4b4b4:
Te6edf3pid Tff7b72= Tff7b72None
Tff7b72if Te6edf3pid Tff7b72not Tff7b72in Te6edf3availableTb4b4b4:
Te6edf3pid Tff7b72= Te6edf3ProfilesTff7b72.Td2a8ffDEFAULT_PROFILE

Te6edf3codec2_profiles Tff7b72= Tb4b4b4{
Te6edf3ProfilesTff7b72.Td2a8ffBANDWIDTH_ULTRA_LOWTb4b4b4,
Te6edf3ProfilesTff7b72.Td2a8ffBANDWIDTH_VERY_LOWTb4b4b4,
Te6edf3ProfilesTff7b72.Td2a8ffBANDWIDTH_LOWTb4b4b4,
Tb4b4b4}
Tff7b72if Te6edf3pid Tff7b72in Te6edf3codec2_profiles Tff7b72and Tff7b72not Tff7b72selfTff7b72.Td2a8ffcodec2_availableTb4b4b4(Tb4b4b4)Tb4b4b4:
Te6edf3RNSTff7b72.Td2a8fflogTb4b4b4(
Ta5d6ff"Ta5d6ffTelephoneManager: Codec2 unavailable, falling back to default Opus profileTa5d6ff"Tb4b4b4,
Te6edf3RNSTff7b72.Td2a8ffLOG_WARNINGTb4b4b4,
Tb4b4b4)
Te6edf3pid Tff7b72= Te6edf3ProfilesTff7b72.Td2a8ffDEFAULT_PROFILE
Tff7b72return Te6edf3pid

Tff7b72def Td2a8ffapply_preferred_profileTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3profile_idTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Store preferred profile and apply it if a call is already established."""
Tff7b72selfTff7b72.Td2a8ffpreferred_profile_id Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_audio_profile_idTb4b4b4(Te6edf3profile_idTb4b4b4)
Tff7b72if Tb4b4b4(
Tff7b72selfTff7b72.Td2a8fftelephone
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff6
Tb4b4b4)Tb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffswitch_profileTb4b4b4(Tff7b72selfTff7b72.Td2a8ffpreferred_profile_idTb4b4b4)
Tff7b72return Tff7b72selfTff7b72.Td2a8ffpreferred_profile_id

Tff7b72def Td2a8ffresolve_call_mode_idTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3mode_idTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Return a valid LXST call mode id (full or half duplex)."""
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backend Tff7b72import Te6edf3lxst_profiles_compat Tff7b72as Te6edf3lxst_modes

Te6edf3available Tff7b72= Tffa657setTb4b4b4(Te6edf3lxst_modesTff7b72.Td2a8ffavailable_modesTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3mid Tff7b72= Te6edf3mode_id
Tff7b72if Te6edf3mid Tff7b72is Tff7b72None Tff7b72and Tff7b72selfTff7b72.Td2a8ffconfig_managerTb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3mid Tff7b72= Tff7b72selfTff7b72.Td2a8ffconfig_managerTff7b72.Td2a8fftelephone_call_mode_idTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3mid Tff7b72= Tffa657intTb4b4b4(Te6edf3midTb4b4b4) Tff7b72if Te6edf3mid Tff7b72is Tff7b72not Tff7b72None Tff7b72else Tff7b72None
Tff7b72except Tb4b4b4(Tf85149TypeErrorTb4b4b4, Tf85149ValueErrorTb4b4b4)Tb4b4b4:
Te6edf3mid Tff7b72= Tff7b72None
Tff7b72if Te6edf3mid Tff7b72not Tff7b72in Te6edf3availableTb4b4b4:
Te6edf3mid Tff7b72= Te6edf3lxst_modesTff7b72.Td2a8ffdefault_modeTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3mid

Tff7b72def Td2a8ffapply_preferred_modeTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3mode_idTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Store preferred duplex mode and apply it on an established call."""
Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_call_mode_idTb4b4b4(Te6edf3mode_idTb4b4b4)
Tff7b72if Tb4b4b4(
Tff7b72selfTff7b72.Td2a8fftelephone
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff6
Tb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffswitch_modeTb4b4b4(Tff7b72selfTff7b72.Td2a8ffpreferred_mode_idTb4b4b4)
Tff7b72return Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id

Tff7b72def Td2a8ffswitch_modeTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3mode_idTb4b4b4)Tb4b4b4:
T8b949e"""Switch live call duplex mode via LXST signalling and local squelch."""
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backend Tff7b72import Te6edf3lxst_profiles_compat Tff7b72as Te6edf3lxst_modes

Te6edf3resolved Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_call_mode_idTb4b4b4(Te6edf3mode_idTb4b4b4)
Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id Tff7b72= Te6edf3resolved
Tff7b72if Tff7b72not Tb4b4b4(
Tff7b72selfTff7b72.Td2a8fftelephone
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff6
Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3resolved
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffswitch_modeTb4b4b4(Te6edf3resolvedTb4b4b4)
T8b949e# Keep local view aligned even when LXST is mocked in tests.
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_callTff7b72.Td2a8ffcall_mode Tff7b72= Te6edf3resolved
T8b949e# Half duplex starts listen-only. Full duplex keeps continuous TX open.
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Tff7b72False
Tff7b72if Te6edf3resolved Tff7b72== Te6edf3lxst_modesTff7b72.Td2a8ffmode_half_duplexTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffsquelch_transmitTb4b4b4(Tff7b72TrueTb4b4b4)
Tff7b72elseTb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffunsquelch_transmitTb4b4b4(Tff7b72TrueTb4b4b4)
Tff7b72return Te6edf3resolved

Tff7b72def Td2a8ffget_active_mode_idTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e"""Return the active call mode, preferred mode, or LXST default."""
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backend Tff7b72import Te6edf3lxst_profiles_compat Tff7b72as Te6edf3lxst_modes

Te6edf3available Tff7b72= Tffa657setTb4b4b4(Te6edf3lxst_modesTff7b72.Td2a8ffavailable_modesTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_callTb4b4b4:
Te6edf3link_mode Tff7b72= Tffa657getattrTb4b4b4(Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_callTb4b4b4, Ta5d6ff"Ta5d6ffcall_modeTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Te6edf3link_mode Tff7b72in Te6edf3availableTb4b4b4:
Tff7b72return Te6edf3link_mode
Te6edf3mode Tff7b72= Tffa657getattrTb4b4b4(Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4, Ta5d6ff"Ta5d6ffactive_modeTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Te6edf3mode Tff7b72in Te6edf3availableTb4b4b4:
Tff7b72return Te6edf3mode
Tff7b72if Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id Tff7b72in Te6edf3availableTb4b4b4:
Tff7b72return Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id
Tff7b72return Tff7b72selfTff7b72.Td2a8ffresolve_call_mode_idTb4b4b4(Tb4b4b4)

Tff7b72def Td2a8ffis_half_duplexTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backend Tff7b72import Te6edf3lxst_profiles_compat Tff7b72as Te6edf3lxst_modes

Tff7b72return Tff7b72selfTff7b72.Td2a8ffget_active_mode_idTb4b4b4(Tb4b4b4) Tff7b72== Te6edf3lxst_modesTff7b72.Td2a8ffmode_half_duplexTb4b4b4(Tb4b4b4)

Tff7b72def Td2a8ffis_transmit_squelchedTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e"""True when LXST packetizer is squelched (half-duplex idle / PTT up)."""
Tff7b72if Tff7b72not Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72or Tff7b72not Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_callTb4b4b4:
Tff7b72return Tff7b72False
Te6edf3packetizer Tff7b72= Tffa657getattrTb4b4b4(Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_callTb4b4b4, Ta5d6ff"Ta5d6ffpacketizerTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Te6edf3packetizer Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72return Tffa657boolTb4b4b4(Tffa657getattrTb4b4b4(Te6edf3packetizerTb4b4b4, Ta5d6ff"Ta5d6ffsquelchedTa5d6ff"Tb4b4b4, Tff7b72FalseTb4b4b4)Tb4b4b4)
Tff7b72return Tff7b72selfTff7b72.Td2a8ffis_half_duplexTb4b4b4(Tb4b4b4) Tff7b72and Tff7b72not Tff7b72selfTff7b72.Td2a8ffptt_active

Tff7b72def Td2a8ffset_ptt_activeTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3activeTb4b4b4: Tffa657boolTb4b4b4)Tb4b4b4:
T8b949e"""Push-to-talk gate for half-duplex calls (unsquelch while held)."""
Tff7b72if Tff7b72not Tb4b4b4(
Tff7b72selfTff7b72.Td2a8fftelephone
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call
Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff6
Tb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Tff7b72False
Tff7b72return Tff7b72False
Tff7b72if Tff7b72not Tff7b72selfTff7b72.Td2a8ffis_half_duplexTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Tff7b72False
Tff7b72return Tff7b72False
Te6edf3want_active Tff7b72= Tffa657boolTb4b4b4(Te6edf3activeTb4b4b4)
Tff7b72tryTb4b4b4:
Tff7b72if Te6edf3want_activeTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffunsquelch_transmitTb4b4b4(Tff7b72TrueTb4b4b4)
Tff7b72elseTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffsquelch_transmitTb4b4b4(Tff7b72TrueTb4b4b4)
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Te6edf3want_active
Tff7b72return Tff7b72True
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Te6edf3RNSTff7b72.Td2a8fflogTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffTelephoneManager: PTT squelch failed: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4, Te6edf3RNSTff7b72.Td2a8ffLOG_ERRORTb4b4b4)
Tff7b72return Tff7b72False

Tff7b72def Td2a8ff_reset_call_audio_controlsTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftransmit_muted Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffreceive_muted Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Tff7b72False
Tff7b72selfTff7b72.Td2a8ffcall_stats Tff7b72= Tb4b4b4{Tb4b4b4}

Tff7b72def Td2a8ffinit_telephoneTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72return
Tff7b72if Tff7b72selfTff7b72.Td2a8ffconfig_manager Tff7b72and Tff7b72not Tff7b72selfTff7b72.Td2a8ffconfig_managerTff7b72.Td2a8fftelephone_enabledTff7b72.Td2a8ffgetTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return

Tff7b72if Tff7b72selfTff7b72.Td2a8ffweb_audio_requiredTb4b4b4:
Tff7b72from T7ee787meshchatxT7ee787.T7ee787srcT7ee787.T7ee787backendT7ee787.T7ee787web_audio_bridge Tff7b72import Tb4b4b4(
Te6edf3install_hostless_lxst_audioTb4b4b4,
Tb4b4b4)

Te6edf3install_hostless_lxst_audioTb4b4b4(Tb4b4b4)

T8b949e# Never enable LXST auto_answer. MeshChatX answers only via explicit
T8b949e# user action or the separate voicemail timer after RINGING.
Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72= Te6edf3TelephoneTb4b4b4(Tff7b72selfTff7b72.Td2a8ffidentityTb4b4b4, Te6edf3auto_answerTff7b72=Tff7b72NoneTb4b4b4)
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffauto_answer Tff7b72= Tff7b72None
T8b949e# Disable busy tone played on caller side when remote side rejects, or doesn't answer
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_busy_tone_timeTb4b4b4(T79c0ff0Tb4b4b4)
T8b949e# Increase connection timeout for slower networks
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_connect_timeoutTb4b4b4(T79c0ff30Tb4b4b4)

T8b949e# LXST switch_profile / switch_mode are no-ops without an established call.
T8b949e# Remember preferred values and pass them into telephone.call() on dial.
Tff7b72selfTff7b72.Td2a8ffpreferred_profile_id Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_audio_profile_idTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_call_mode_idTb4b4b4(Tb4b4b4)

Tff7b72selfTff7b72.Td2a8ff_install_link_table_cleanupTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ffrefresh_call_policyTb4b4b4(Tb4b4b4)

Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_ringing_callbackTb4b4b4(Tff7b72selfTff7b72.Td2a8ffon_telephone_ringingTb4b4b4)
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_established_callbackTb4b4b4(Tff7b72selfTff7b72.Td2a8ffon_telephone_call_establishedTb4b4b4)
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_ended_callbackTb4b4b4(Tff7b72selfTff7b72.Td2a8ffon_telephone_call_endedTb4b4b4)

Tff7b72def Td2a8ffset_call_policyTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3allowed_fnTff7b72=Tff7b72NoneTb4b4b4, Te6edf3blocked_identity_hashesTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Install LXST-level allow/block checks used before RINGING.

allowed_fn receives the caller identity hash as bytes and must return bool.
blocked_identity_hashes is an optional iterable of identity hash bytes.
"""
Tff7b72selfTff7b72.Td2a8ff_caller_allowed Tff7b72= Te6edf3allowed_fn
Tff7b72if Te6edf3blocked_identity_hashes Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_blocked_identity_hashes Tff7b72= Tff7b72None
Tff7b72elseTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_blocked_identity_hashes Tff7b72= Tb4b4b4[
Tffa657bytesTb4b4b4(Te6edf3hTb4b4b4) Tff7b72for Te6edf3h Tff7b72in Te6edf3blocked_identity_hashes Tff7b72if Te6edf3h Tff7b72is Tff7b72not Tff7b72None
Tb4b4b4]
Tff7b72selfTff7b72.Td2a8ffrefresh_call_policyTb4b4b4(Tb4b4b4)

Tff7b72def Td2a8ffrefresh_call_policyTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e"""Push current policy into the live Telephone instance."""
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return

Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffauto_answer Tff7b72= Tff7b72None

Tff7b72if Tff7b72selfTff7b72.Td2a8ff_blocked_identity_hashes Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_blockedTb4b4b4(Tffa657listTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_blocked_identity_hashesTb4b4b4)Tb4b4b4)
Tff7b72elseTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_blockedTb4b4b4(Tff7b72NoneTb4b4b4)

Tff7b72if Tffa657callableTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_caller_allowedTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_allowedTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_caller_allowedTb4b4b4)
Tff7b72elseTb4b4b4:
T8b949e# Fail closed until MeshChatX installs sync_telephone_call_policy.
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffset_allowedTb4b4b4(Te6edf3TelephoneTff7b72.Td2a8ffALLOW_NONETb4b4b4)

Tff7b72def Td2a8ff_install_link_table_cleanupTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e"""Ensure closed inbound links are removed from Telephone.links.

Older LXST builds never popped self.links on close. Patch the bound
handler so MeshChatX stays safe even before the LXST bump is installed.
"""
Te6edf3phone Tff7b72= Tff7b72selfTff7b72.Td2a8fftelephone
Tff7b72if Te6edf3phone Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72return
Te6edf3previous Tff7b72= Tffa657getattrTb4b4b4(Te6edf3phoneTb4b4b4, Ta5d6ff"Ta5d6ff_Telephone__link_closedTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Tb4b4b4(
Te6edf3previous Tff7b72is Tff7b72None
Tff7b72or Tffa657getattrTb4b4b4(Te6edf3previousTb4b4b4, Ta5d6ff"Ta5d6ff_meshchatx_link_cleanupTa5d6ff"Tb4b4b4, Tff7b72FalseTb4b4b4) Tff7b72is Tff7b72True
Tb4b4b4)Tb4b4b4:
Tff7b72return

Tff7b72def Td2a8ff_link_closedTb4b4b4(Te6edf3linkTb4b4b4, Te6edf3previousTff7b72=Te6edf3previousTb4b4b4, Te6edf3phoneTff7b72=Te6edf3phoneTb4b4b4)Tb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3previousTb4b4b4(Te6edf3linkTb4b4b4)
Tff7b72finallyTb4b4b4:
Te6edf3link_id Tff7b72= Tffa657getattrTb4b4b4(Te6edf3linkTb4b4b4, Ta5d6ff"Ta5d6fflink_idTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Te6edf3link_id Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3phoneTff7b72.Td2a8fflinksTff7b72.Td2a8ffpopTb4b4b4(Te6edf3link_idTb4b4b4, Tff7b72NoneTb4b4b4)

Te6edf3_link_closedTff7b72.Td2a8ff_meshchatx_link_cleanup Tff7b72= Tff7b72True
Te6edf3phoneTff7b72.Td2a8ff_Telephone__link_closed Tff7b72= Te6edf3_link_closed

Tff7b72def Td2a8ffteardownTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72is Tff7b72not Tff7b72NoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffteardownTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72= Tff7b72None

Tff7b72def Td2a8ffhangupTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffhangupTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Te6edf3RNSTff7b72.Td2a8fflogTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffTelephoneManager: Error during hangup: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4, Te6edf3RNSTff7b72.Td2a8ffLOG_ERRORTb4b4b4)

Tff7b72def Td2a8ffrequest_hangupTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e# FIXME: Remove async hangup shim when LXST call() cancellation is non-blocking.
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72if Tff7b72not Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72return
Te6edf3threadingTff7b72.Td2a8ffThreadTb4b4b4(Te6edf3targetTff7b72=Tff7b72selfTff7b72.Td2a8ffhangupTb4b4b4, Te6edf3daemonTff7b72=Tff7b72TrueTb4b4b4)Tff7b72.Td2a8ffstartTb4b4b4(Tb4b4b4)

Tff7b72def Td2a8ffregister_ringing_callbackTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3callbackTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_ringing_callback Tff7b72= Te6edf3callback

Tff7b72def Td2a8ffregister_established_callbackTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3callbackTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_established_callback Tff7b72= Te6edf3callback

Tff7b72def Td2a8ffregister_ended_callbackTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3callbackTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_ended_callback Tff7b72= Te6edf3callback

Tff7b72def Td2a8ffset_callbacksTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3ringingTff7b72=Tff7b72NoneTb4b4b4, Te6edf3establishedTff7b72=Tff7b72NoneTb4b4b4, Te6edf3endedTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3ringingTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffregister_ringing_callbackTb4b4b4(Te6edf3ringingTb4b4b4)
Tff7b72if Te6edf3establishedTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffregister_established_callbackTb4b4b4(Te6edf3establishedTb4b4b4)
Tff7b72if Te6edf3endedTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffregister_ended_callbackTb4b4b4(Te6edf3endedTb4b4b4)

Tff7b72def Td2a8ffon_telephone_ringingTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3caller_identityTb4b4b4: Te6edf3RNSTff7b72.Td2a8ffIdentityTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8ffinitiation_statusTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffRinging...Ta5d6ff"Tb4b4b4)
Tff7b72return

Tff7b72selfTff7b72.Td2a8ffcall_start_time Tff7b72= Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ffcall_is_incoming Tff7b72= Tff7b72True
Tff7b72selfTff7b72.Td2a8ffcall_was_established Tff7b72= Tff7b72False
Tff7b72if Tff7b72selfTff7b72.Td2a8ffon_ringing_callbackTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_ringing_callbackTb4b4b4(Te6edf3caller_identityTb4b4b4)

Tff7b72def Td2a8ffon_telephone_call_establishedTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3caller_identityTb4b4b4: Te6edf3RNSTff7b72.Td2a8ffIdentityTb4b4b4)Tb4b4b4:
T8b949e# Update start time to when it was actually established for duration calculation
Tff7b72selfTff7b72.Td2a8ffcall_start_time Tff7b72= Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ffcall_was_established Tff7b72= Tff7b72True
Tff7b72selfTff7b72.Td2a8ffptt_active Tff7b72= Tff7b72False

T8b949e# Track per-call stats from the active link (uses RNS Link counters)
Te6edf3link Tff7b72= Tffa657getattrTb4b4b4(Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4, Ta5d6ff"Ta5d6ffactive_callTa5d6ff"Tb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72selfTff7b72.Td2a8ffcall_stats Tff7b72= Tb4b4b4{
Ta5d6ff"Ta5d6fflinkTa5d6ff"Tb4b4b4: Te6edf3linkTb4b4b4,
Ta5d6ff"Ta5d6ffstarted_atTa5d6ff"Tb4b4b4: Tff7b72selfTff7b72.Td2a8ffcall_start_timeTb4b4b4,
Tb4b4b4}

Tff7b72if Tff7b72selfTff7b72.Td2a8ffon_established_callbackTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_established_callbackTb4b4b4(Te6edf3caller_identityTb4b4b4)

Tff7b72def Td2a8ffon_telephone_call_endedTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3caller_identityTb4b4b4: Te6edf3RNSTff7b72.Td2a8ffIdentityTb4b4b4)Tb4b4b4:
T8b949e# Capture status just before ending if possible, or use the last known status
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffcall_status_at_end Tff7b72= Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status

T8b949e# Ensure initiation status is cleared when call ends
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72selfTff7b72.Td2a8ff_reset_call_audio_controlsTb4b4b4(Tb4b4b4)

Tff7b72if Tff7b72selfTff7b72.Td2a8ffon_ended_callbackTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_ended_callbackTb4b4b4(Te6edf3caller_identityTb4b4b4)

Tff7b72def Td2a8ffstart_recordingTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e# Disabled for now as LXST does not have a Tee to use
Tff7b72pass

Tff7b72def Td2a8ffstop_recordingTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
T8b949e# Disabled for now
Tff7b72pass

Tff7b72def Td2a8ffannounceTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3attached_interfaceTff7b72=Tff7b72NoneTb4b4b4, Te6edf3display_nameTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72if Te6edf3display_nameTb4b4b4:
Tff7b72import T7ee787RNST7ee787.T7ee787vendorT7ee787.T7ee787umsgpack Tff7b72as T7ee787msgpack

T8b949e# Pack display name in LXMF-compatible app data format
Te6edf3app_data Tff7b72= Te6edf3msgpackTff7b72.Td2a8ffpackbTb4b4b4(Tb4b4b4[Te6edf3display_nameTb4b4b4, Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4]Tb4b4b4)
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffdestinationTff7b72.Td2a8ffannounceTb4b4b4(
Te6edf3app_dataTff7b72=Te6edf3app_dataTb4b4b4,
Te6edf3attached_interfaceTff7b72=Te6edf3attached_interfaceTb4b4b4,
Tb4b4b4)
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8fflast_announce Tff7b72= Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4)
Tff7b72elseTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffannounceTb4b4b4(Te6edf3attached_interfaceTff7b72=Te6edf3attached_interfaceTb4b4b4)

Tff7b72def Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3statusTb4b4b4, Te6edf3target_hashTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ffinitiation_status Tff7b72= Te6edf3status
Tff7b72if Te6edf3target_hash Tff7b72is Tff7b72not Tff7b72None Tff7b72or Te6edf3status Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffinitiation_target_hash Tff7b72= Te6edf3target_hash
Tff7b72if Tff7b72selfTff7b72.Td2a8ffon_initiation_status_callbackTb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72selfTff7b72.Td2a8ffon_initiation_status_callbackTb4b4b4(
Tff7b72selfTff7b72.Td2a8ffinitiation_statusTb4b4b4,
Tff7b72selfTff7b72.Td2a8ffinitiation_target_hashTb4b4b4,
Tb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Te6edf3RNSTff7b72.Td2a8fflogTb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ffTelephoneManager: Error in initiation status callback: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4,
Te6edf3RNSTff7b72.Td2a8ffLOG_ERRORTb4b4b4,
Tb4b4b4)

Tff7b72def Td2a8ff_is_initiation_cancelledTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72not Tffa657boolTb4b4b4(Tff7b72selfTff7b72.Td2a8ffinitiation_statusTb4b4b4)

Tff7b72async Tff7b72def Td2a8ff_await_pathTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3destination_hashTb4b4b4: Tffa657bytesTb4b4b4, Te6edf3timeout_secondsTb4b4b4: Tffa657floatTb4b4b4)Tb4b4b4:
T8b949e# Reuse shared pathfinding behavior so stale/unresponsive routes are
T8b949e# refreshed before we wait, mirroring the faster outbound LXMF path prep.
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3reticulum_pathfindingTff7b72.Td2a8ffprepare_fresh_path_requestTb4b4b4(Tff7b72NoneTb4b4b4, Te6edf3destination_hashTb4b4b4)

Te6edf3timeout_after Tff7b72= Te6edf3timeTff7b72.Td2a8ffmonotonicTb4b4b4(Tb4b4b4) Tff7b72+ Tffa657maxTb4b4b4(T79c0ff0.0Tb4b4b4, Te6edf3timeout_secondsTb4b4b4)
Te6edf3next_request_at Tff7b72= T79c0ff0.0

Tff7b72while Te6edf3timeTff7b72.Td2a8ffmonotonicTb4b4b4(Tb4b4b4) Tff7b72< Te6edf3timeout_afterTb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8ff_is_initiation_cancelledTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72False

Tff7b72if Te6edf3RNSTff7b72.Td2a8ffTransportTff7b72.Td2a8ffhas_pathTb4b4b4(Te6edf3destination_hashTb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72True

Te6edf3now Tff7b72= Te6edf3timeTff7b72.Td2a8ffmonotonicTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3now Tff7b72>Tff7b72= Te6edf3next_request_atTb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3reticulum_pathfindingTff7b72.Td2a8ffnudge_path_requestTb4b4b4(Te6edf3destination_hashTb4b4b4)
Te6edf3next_request_at Tff7b72= Te6edf3now Tff7b72+ Tff7b72selfTff7b72.Td2a8ff_path_retry_interval_s

Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_path_poll_interval_sTb4b4b4)

Tff7b72return Te6edf3RNSTff7b72.Td2a8ffTransportTff7b72.Td2a8ffhas_pathTb4b4b4(Te6edf3destination_hashTb4b4b4)

Tff7b72async Tff7b72def Td2a8ffinitiateTb4b4b4(Tff7b72selfTb4b4b4, Te6edf3destination_hashTb4b4b4: Tffa657bytesTb4b4b4, Te6edf3timeout_secondsTb4b4b4: Tffa657int Tff7b72= T79c0ff15Tb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72is Tff7b72NoneTb4b4b4:
Te6edf3msg Tff7b72= Ta5d6ff"Ta5d6ffTelephone is not initializedTa5d6ff"
Tff7b72raise Tf85149RuntimeErrorTb4b4b4(Te6edf3msgTb4b4b4)

Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffbusy Tff7b72or Tff7b72selfTff7b72.Td2a8ffinitiation_statusTb4b4b4:
Te6edf3msg Tff7b72= Ta5d6ff"Ta5d6ffTelephone is already in useTa5d6ff"
Tff7b72raise Tf85149RuntimeErrorTb4b4b4(Te6edf3msgTb4b4b4)

Te6edf3destination_hash_hex Tff7b72= Te6edf3destination_hashTff7b72.Td2a8ffhexTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffResolving identity...Ta5d6ff"Tb4b4b4, Te6edf3destination_hash_hexTb4b4b4)

Tff7b72tryTb4b4b4:

Tff7b72def Td2a8ffresolve_identityTb4b4b4(Te6edf3target_hash_hexTb4b4b4)Tb4b4b4:
T8b949e"""Resolve identity from multiple hints: direct recall, destination_hash announce, identity_hash announce, or public key."""
Te6edf3target_hash Tff7b72= Te6edf3hex_identifier_to_bytesTb4b4b4(Te6edf3target_hash_hexTb4b4b4)
Tff7b72if Tff7b72not Te6edf3target_hashTb4b4b4:
Tff7b72return Tff7b72None

T8b949e# 1) Direct recall (identity hash)
Te6edf3ident Tff7b72= Te6edf3RNSTff7b72.Td2a8ffIdentityTff7b72.Td2a8ffrecallTb4b4b4(Te6edf3target_hashTb4b4b4)
Tff7b72if Te6edf3identTb4b4b4:
Tff7b72return Te6edf3ident

Tff7b72if Tff7b72not Tff7b72selfTff7b72.Td2a8ffdbTb4b4b4:
Tff7b72return Tff7b72None

Te6edf3th Tff7b72= Te6edf3target_hash_hexTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3canonical Tff7b72= Te6edf3normalize_hex_identifierTb4b4b4(Te6edf3thTb4b4b4)

T8b949e# 2) By destination_hash (could be lxst.telephony or lxmf.delivery hash)
Te6edf3announce Tff7b72= Tff7b72selfTff7b72.Td2a8ffdbTff7b72.Td2a8ffannouncesTff7b72.Td2a8ffget_announce_by_hashTb4b4b4(Te6edf3thTb4b4b4)
Tff7b72if Tff7b72not Te6edf3announce Tff7b72and Te6edf3canonicalTb4b4b4:
Te6edf3announce Tff7b72= Tff7b72selfTff7b72.Td2a8ffdbTff7b72.Td2a8ffannouncesTff7b72.Td2a8ffget_announce_by_hashTb4b4b4(Te6edf3canonicalTb4b4b4)
Tff7b72if Tff7b72not Te6edf3announceTb4b4b4:
T8b949e# 3) By identity_hash field (if user entered identity hash but we missed recall, or other announce types)
Te6edf3id_key Tff7b72= Te6edf3canonical Tff7b72or Te6edf3th
Te6edf3announces Tff7b72= Tff7b72selfTff7b72.Td2a8ffdbTff7b72.Td2a8ffannouncesTff7b72.Td2a8ffget_filtered_announcesTb4b4b4(
Te6edf3identity_hashTff7b72=Te6edf3id_keyTb4b4b4,
Tb4b4b4)
Tff7b72if Te6edf3announcesTb4b4b4:
Te6edf3announce Tff7b72= Te6edf3announcesTb4b4b4[T79c0ff0Tb4b4b4]

Tff7b72if Tff7b72not Te6edf3announceTb4b4b4:
Tff7b72return Tff7b72None

T8b949e# Try identity_hash from announce
Te6edf3identity_hex Tff7b72= Te6edf3announceTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffidentity_hashTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3identity_hexTb4b4b4:
Te6edf3id_bytes Tff7b72= Te6edf3hex_identifier_to_bytesTb4b4b4(Te6edf3identity_hexTb4b4b4)
Tff7b72if Te6edf3id_bytesTb4b4b4:
Te6edf3ident Tff7b72= Te6edf3RNSTff7b72.Td2a8ffIdentityTff7b72.Td2a8ffrecallTb4b4b4(Te6edf3id_bytesTb4b4b4)
Tff7b72if Te6edf3identTb4b4b4:
Tff7b72return Te6edf3ident

T8b949e# Try reconstructing from public key
Tff7b72if Te6edf3announceTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffidentity_public_keyTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3RNSTff7b72.Td2a8ffIdentityTff7b72.Td2a8fffrom_bytesTb4b4b4(
Te6edf3base64Tff7b72.Td2a8ffb64decodeTb4b4b4(Te6edf3announceTb4b4b4[Ta5d6ff"Ta5d6ffidentity_public_keyTa5d6ff"Tb4b4b4]Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Tff7b72return Tff7b72None

T8b949e# Find destination identity
Te6edf3destination_identity Tff7b72= Te6edf3resolve_identityTb4b4b4(Te6edf3destination_hash_hexTb4b4b4)

Tff7b72if Te6edf3destination_identity Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffDiscovering path/identity...Ta5d6ff"Tb4b4b4)
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3reticulum_pathfindingTff7b72.Td2a8ffprepare_fresh_path_requestTb4b4b4(
Tff7b72NoneTb4b4b4,
Te6edf3destination_hashTb4b4b4,
Tb4b4b4)
Te6edf3timeout_after Tff7b72= Te6edf3timeTff7b72.Td2a8ffmonotonicTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3timeout_seconds
Te6edf3next_request_at Tff7b72= T79c0ff0.0

T8b949e# Wait for identity to appear while also nudging path discovery.
Tff7b72while Te6edf3timeTff7b72.Td2a8ffmonotonicTb4b4b4(Tb4b4b4) Tff7b72< Te6edf3timeout_afterTb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8ff_is_initiation_cancelledTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72None

Te6edf3now Tff7b72= Te6edf3timeTff7b72.Td2a8ffmonotonicTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3now Tff7b72>Tff7b72= Te6edf3next_request_atTb4b4b4:
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3reticulum_pathfindingTff7b72.Td2a8ffnudge_path_requestTb4b4b4(Te6edf3destination_hashTb4b4b4)
Te6edf3next_request_at Tff7b72= Te6edf3now Tff7b72+ Tff7b72selfTff7b72.Td2a8ff_path_retry_interval_s

Te6edf3destination_identity Tff7b72= Te6edf3resolve_identityTb4b4b4(Te6edf3destination_hash_hexTb4b4b4)
Tff7b72if Te6edf3destination_identityTb4b4b4:
Tff7b72break
Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_path_poll_interval_sTb4b4b4)

Tff7b72if Te6edf3destination_identity Tff7b72is Tff7b72NoneTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)
Te6edf3msg Tff7b72= Ta5d6ff"Ta5d6ffDestination identity not foundTa5d6ff"
Tff7b72raise Tf85149RuntimeErrorTb4b4b4(Te6edf3msgTb4b4b4)

T8b949e# FIXME: Remove telephony-destination pre-path lookup once LXST aligns
T8b949e# identity-hash and telephony-destination path handling.
Te6edf3call_destination_hashTb4b4b4: Tffa657bytes Tff7b72= Te6edf3destination_hash
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
Te6edf3dest Tff7b72= Te6edf3RNSTff7b72.Td2a8ffDestinationTb4b4b4(
Te6edf3destination_identityTb4b4b4,
Te6edf3RNSTff7b72.Td2a8ffDestinationTff7b72.Td2a8ffOUTTb4b4b4,
Te6edf3RNSTff7b72.Td2a8ffDestinationTff7b72.Td2a8ffSINGLETb4b4b4,
Ta5d6ff"Ta5d6fflxstTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fftelephonyTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3dest_hash Tff7b72= Te6edf3destTff7b72.Td2a8ffhash
Tff7b72if Tffa657isinstanceTb4b4b4(Te6edf3dest_hashTb4b4b4, Tffa657bytesTb4b4b4)Tb4b4b4:
Te6edf3call_destination_hash Tff7b72= Te6edf3dest_hash

Tff7b72if Tff7b72not Te6edf3RNSTff7b72.Td2a8ffTransportTff7b72.Td2a8ffhas_pathTb4b4b4(Te6edf3call_destination_hashTb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffRequesting path...Ta5d6ff"Tb4b4b4)
Te6edf3has_path Tff7b72= Tff7b72await Tff7b72selfTff7b72.Td2a8ff_await_pathTb4b4b4(
Te6edf3call_destination_hashTb4b4b4,
Te6edf3timeout_secondsTff7b72=Tffa657minTb4b4b4(Te6edf3timeout_secondsTb4b4b4, T79c0ff10Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tff7b72if Tff7b72selfTff7b72.Td2a8ff_is_initiation_cancelledTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72None
Tff7b72if Tff7b72not Te6edf3has_pathTb4b4b4:
Te6edf3msg Tff7b72= Ta5d6ff"Ta5d6ffPath not found to destinationTa5d6ff"
Tff7b72raise Tf85149RuntimeErrorTb4b4b4(Te6edf3msgTb4b4b4)

Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffEstablishing link...Ta5d6ff"Tb4b4b4, Te6edf3destination_hash_hexTb4b4b4)
Tff7b72selfTff7b72.Td2a8ffcall_start_time Tff7b72= Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ffcall_is_incoming Tff7b72= Tff7b72False

Te6edf3profile_id Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_audio_profile_idTb4b4b4(Tff7b72selfTff7b72.Td2a8ffpreferred_profile_idTb4b4b4)
Tff7b72selfTff7b72.Td2a8ffpreferred_profile_id Tff7b72= Te6edf3profile_id
Te6edf3mode_id Tff7b72= Tff7b72selfTff7b72.Td2a8ffresolve_call_mode_idTb4b4b4(Tff7b72selfTff7b72.Td2a8ffpreferred_mode_idTb4b4b4)
Tff7b72selfTff7b72.Td2a8ffpreferred_mode_id Tff7b72= Te6edf3mode_id

T8b949e# Use a thread for the blocking LXST call, but monitor status for early exit
T8b949e# if established elsewhere or timed out/hung up. Pass preferred profile and
T8b949e# duplex mode so Codec2/Opus and half-duplex actually apply on dial.
Te6edf3call_task Tff7b72= Te6edf3asyncioTff7b72.Td2a8ffcreate_taskTb4b4b4(
Te6edf3asyncioTff7b72.Td2a8ffto_threadTb4b4b4(
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcallTb4b4b4,
Te6edf3destination_identityTb4b4b4,
Te6edf3profile_idTb4b4b4,
Te6edf3mode_idTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3start_wait Tff7b72= Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4)
Te6edf3cancel_requested Tff7b72= Tff7b72False
T8b949e# LXST telephone.call usually returns on establishment or timeout.
T8b949e# We wait for it, but if status becomes established or ended, we can stop waiting.
Tff7b72while Tff7b72not Te6edf3call_taskTff7b72.Td2a8ffdoneTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8ff_is_initiation_cancelledTb4b4b4(Tb4b4b4)Tb4b4b4:
Te6edf3cancel_requested Tff7b72= Tff7b72True
Tff7b72break

T8b949e# Update UI status based on current call state
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff2Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffCalling...Ta5d6ff"Tb4b4b4, Te6edf3destination_hash_hexTb4b4b4)
Tff7b72elif Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff4Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6ff"Ta5d6ffRinging...Ta5d6ff"Tb4b4b4, Te6edf3destination_hash_hexTb4b4b4)
Tff7b72elif Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff5Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(
Ta5d6ff"Ta5d6ffEstablishing link...Ta5d6ff"Tb4b4b4,
Te6edf3destination_hash_hexTb4b4b4,
Tb4b4b4)

Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72in Tb4b4b4[
T79c0ff6Tb4b4b4,
T79c0ff0Tb4b4b4,
T79c0ff1Tb4b4b4,
Tb4b4b4]Tb4b4b4: T8b949e# Established, Busy, Rejected
Tff7b72break
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff3 Tff7b72and Tb4b4b4(
Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4) Tff7b72- Te6edf3start_wait Tff7b72> T79c0ff1.0
Tb4b4b4)Tb4b4b4: T8b949e# Available (ended/timeout)
Tff7b72break
Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_status_poll_interval_sTb4b4b4)

Tff7b72if Te6edf3cancel_requestedTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72with Te6edf3contextlibTff7b72.Td2a8ffsuppressTb4b4b4(Tf85149ExceptionTb4b4b4)Tb4b4b4:
T8b949e# FIXME: Remove async hangup dispatch when LXST exposes cooperative cancellation.
Te6edf3asyncioTff7b72.Td2a8ffcreate_taskTb4b4b4(Te6edf3asyncioTff7b72.Td2a8ffto_threadTb4b4b4(Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffhangupTb4b4b4)Tb4b4b4)
Tff7b72return Tff7b72None

T8b949e# If the task finished but we're still ringing or connecting,
T8b949e# wait a bit more for establishment or definitive failure
Tff7b72if Tff7b72selfTff7b72.Td2a8ffinitiation_status Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72in Tb4b4b4[
T79c0ff2Tb4b4b4,
T79c0ff4Tb4b4b4,
T79c0ff5Tb4b4b4,
Tb4b4b4]Tb4b4b4: T8b949e# Calling, Ringing, Connecting
Te6edf3wait_until Tff7b72= Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3timeout_seconds
Tff7b72while Te6edf3timeTff7b72.Td2a8fftimeTb4b4b4(Tb4b4b4) Tff7b72< Te6edf3wait_untilTb4b4b4:
Tff7b72if Tff7b72not Tff7b72selfTff7b72.Td2a8ffinitiation_statusTb4b4b4: T8b949e# Externally cancelled
Tff7b72break

Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff2Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(
Ta5d6ff"Ta5d6ffCalling...Ta5d6ff"Tb4b4b4,
Te6edf3destination_hash_hexTb4b4b4,
Tb4b4b4)
Tff7b72elif Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff4Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(
Ta5d6ff"Ta5d6ffRinging...Ta5d6ff"Tb4b4b4,
Te6edf3destination_hash_hexTb4b4b4,
Tb4b4b4)
Tff7b72elif Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff5Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(
Ta5d6ff"Ta5d6ffEstablishing link...Ta5d6ff"Tb4b4b4,
Te6edf3destination_hash_hexTb4b4b4,
Tb4b4b4)

Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72in Tb4b4b4[
T79c0ff6Tb4b4b4,
T79c0ff0Tb4b4b4,
T79c0ff1Tb4b4b4,
T79c0ff3Tb4b4b4,
Tb4b4b4]Tb4b4b4: T8b949e# Established, Busy, Rejected, Ended
Tff7b72break
Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_status_poll_interval_sTb4b4b4)

Tff7b72return Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call

Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffFailed: Tffd700{Te6edf3eTffd700!s}Ta5d6ff"Tb4b4b4)
Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff3Tb4b4b4)
Tff7b72raise
Tff7b72finallyTb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8ff_is_initiation_cancelledTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)
Tff7b72elseTb4b4b4:
T8b949e# Wait for either establishment, failure, or a timeout
T8b949e# to ensure the UI has something to show (either active_call or initiation_status)
Tff7b72for Te6edf3_ Tff7b72in Tffa657rangeTb4b4b4(T79c0ff40Tb4b4b4)Tb4b4b4: T8b949e# Max 4 seconds of defensive waiting
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72and Tb4b4b4(
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call
Tff7b72or Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72in Tb4b4b4[T79c0ff0Tb4b4b4, T79c0ff1Tb4b4b4, T79c0ff3Tb4b4b4, T79c0ff6Tb4b4b4]
Tb4b4b4)Tb4b4b4:
Tff7b72break
Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(Tff7b72selfTff7b72.Td2a8ff_status_poll_interval_sTb4b4b4)

T8b949e# If call was successful, keep status for a moment to prevent UI flicker
T8b949e# while the frontend picks up the new active_call state
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephone Tff7b72and Tb4b4b4(
Tb4b4b4(Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffactive_call Tff7b72and Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72== T79c0ff6Tb4b4b4)
Tff7b72or Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffcall_status Tff7b72in Tb4b4b4[T79c0ff2Tb4b4b4, T79c0ff4Tb4b4b4, T79c0ff5Tb4b4b4]
Tb4b4b4)Tb4b4b4:
Tff7b72await Te6edf3asyncioTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff1.0Tb4b4b4)
Tff7b72selfTff7b72.Td2a8ff_update_initiation_statusTb4b4b4(Tff7b72NoneTb4b4b4, Tff7b72NoneTb4b4b4)

Tff7b72def Td2a8ffmute_transmitTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffmute_transmitTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72selfTff7b72.Td2a8fftransmit_muted Tff7b72= Tff7b72True

Tff7b72def Td2a8ffunmute_transmitTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffunmute_transmitTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72selfTff7b72.Td2a8fftransmit_muted Tff7b72= Tff7b72False

Tff7b72def Td2a8ffmute_receiveTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffmute_receiveTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72selfTff7b72.Td2a8ffreceive_muted Tff7b72= Tff7b72True

Tff7b72def Td2a8ffunmute_receiveTb4b4b4(Tff7b72selfTb4b4b4)Tb4b4b4:
Tff7b72if Tff7b72selfTff7b72.Td2a8fftelephoneTb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72selfTff7b72.Td2a8fftelephoneTff7b72.Td2a8ffunmute_receiveTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72selfTff7b72.Td2a8ffreceive_muted Tff7b72= Tff7b72False


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────